package de.kardass.jannobatch.manager;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import de.kardass.jannobatch.ItemProcessor;
import de.kardass.jannobatch.ItemReader;
import de.kardass.jannobatch.ItemWriter;
import de.kardass.jannobatch.annotation.Batch;
import de.kardass.jannobatch.annotation.BatchException;
import de.kardass.jannobatch.annotation.BatchJob;
import de.kardass.jannobatch.annotation.BatchStep;
import de.kardass.jannobatch.annotation.NullItemReader;
import de.kardass.jannobatch.annotation.NullItemWriter;
import de.kardass.jannobatch.errorhandler.ErrorHandler;


/**
 * @author Manfred Kardass (manfred@kardass.de)
 *
 */
public class BatchManager {
	
	private final List<BatchWorkflow> batchComponents = new ArrayList<BatchWorkflow>();
	
	private Class<?> batchRoot;
	
	/**
	 * @param batchRoot
	 */
	public BatchManager(Class<?> batchRoot) {
		this.batchRoot = batchRoot;
	}

	/**
	 * Configures the batch workflow(s).
	 */
	public static <T> BatchManager configure(Class<T> batchRoot) {
		return new BatchManager(batchRoot).configure();
	}

	/**
	 * Configures the batch workflow(s).
	 */
	public BatchManager configure() {
		final Batch batch = batchRoot.getAnnotation(Batch.class);
		final BatchJob batchJob = batchRoot.getAnnotation(BatchJob.class);
		if (batch == null && batchJob == null) {
			throw new IllegalArgumentException("Neither Batch nor BatchJob annotation found");
		}
		if (batch != null && batchJob != null) {
			throw new IllegalArgumentException("Only one annotation allowed (Batch or BatchJob)");
		}
		final BatchJob[] jobs = (batch == null) ? new BatchJob[] {batchJob} : batch.jobs();
		BatchWorkflow workflow;
		for (BatchJob aJob : jobs) {
			workflow = configureWorkflow(0, aJob);
			BatchWorkflowValidator.validate(workflow);
			if (workflow != null) {
				batchComponents.add(workflow);
			}
		}
		return this;
	}

	/**
	 * Starts the batch workflow(s).
	 */
	public void startBatch() {
		for (BatchWorkflow component : batchComponents) {
			component.process();
		}
	}

	/**
	 * Configures the batch workflow starting at the given level.
	 */
	private BatchWorkflow configureWorkflow(int level, BatchJob job) {
		final BatchStep[] steps = job.steps();
		final ItemReader<?> reader = newInstanceIfNotNull(job.reader());
		if (reader == null) {
			throw new IllegalArgumentException("No item reader configured");
		}
		final ItemWriter<?> writer = newInstanceIfNotNull(job.writer());
		ItemProcessor<?,?> module;
		List<ItemProcessor<?,?>> processors = new ArrayList<ItemProcessor<?,?>>();
		for (BatchStep aStep : steps) {
			module = newInstanceIfNotNull(aStep.module());
			processors.add(module);
		}
		HashMap<Class<? extends Throwable>, ErrorHandler<?, ? extends Throwable>> errorHandlerMap = getErrorHandlers(job);
		final BatchWorkflow component = new BatchWorkflow(reader, processors, writer, errorHandlerMap);
		return component;
	}

	/**
	 * Reads the annotated exceptions' error handlers and returns their mapping
	 * as a map.
	 * @param <T>
	 * @param job
	 * @return
	 */
	private HashMap<Class<? extends Throwable>, ErrorHandler<?, ? extends Throwable>> getErrorHandlers(BatchJob job) {
		final HashMap<Class<? extends Throwable>, ErrorHandler<?, ? extends Throwable>> result = 
		    new HashMap<Class<? extends Throwable>, ErrorHandler<?, ? extends Throwable>>();
		final BatchException[] batchExceptions = job.exceptions();
		ErrorHandler<?, ? extends Throwable> handler;
		for (BatchException aException : batchExceptions) {
			Class<? extends Throwable> exception = aException.exception();
			Class<? extends ErrorHandler<?, ? extends Throwable>> beh = aException.handler();
			handler = newInstanceIfNotNull(beh);
			result.put(exception, handler);
		}
		return result;
	}

	/**
	 * Creates a new instance if the given class is not null.
	 * @param <T>
	 * @param clazz
	 * @return
	 */
	private <T> T newInstanceIfNotNull(Class<T> clazz) {
		if (clazz == null || clazz.equals(NullItemReader.class) || clazz.equals(NullItemWriter.class)) {
			return null;
		}
		try {
			Constructor<T> constructor = clazz.getConstructor((Class[]) null);
			return constructor.newInstance((Object[]) null);
		} catch (Exception e) {
			throw new RuntimeException(e.getCause());
		}
	}

}
